## Дескрипторы классов

интересная, но не простая тему.
У объектов-свойств есть  явный недостаток.
Давайте представим, что создаем класс для представления точек в трехмерном пространстве:

```python
class Point3D:
    def __init__(self, x, y, z):
        self._x = x
        self._y = y
        self._z = z
```

Здесь у нас формируются защищенные локальные свойства для создаваемого объекта класса `Point3D`. 

Теперь представим, что согласно заданию координаты должны представляться исключительно целыми числами. 

Для этого реализуем следующий метод проверки:

```python
    @classmethod
    def verify_coord(cls, coord):
        if type(coord) != int:
            raise TypeError("Координата должна быть целым числом")
```
А вызывать его будут в сеттерах соответствующих свойств класса:

```python
    @property
    def x(self):
        return self._x
 
    @x.setter
    def x(self, coord):
        self.verify_coord(coord)
        self._x = coord
 
    @property
    def y(self):
        return self._y
 
    @y.setter
    def y(self, coord):
        self.verify_coord(coord)
        self._y = coord
 
    @property
    def z(self):
        return self._z
 
    @z.setter
    def z(self, coord):
        self.verify_coord(coord)
        self._z = coord

```

Здесь все вам должно быть понятно. 

И теперь мы можем в инициализаторе использовать эти объекты-свойства для формирования локальных атрибутов экземпляров:

```python
class Point3D:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
```
Все, создавая объекты этого класса:

```python
p = Point3D(1, 2, 3)
print(p.__dict__)

```
у нас автоматически будут формироваться нужные локальные атрибуты и мы сможем с ними работать через объекты-свойства `x, y, z`.

Но, смотрите, в нашем классе `Point3D` получилось своеобразное дублирование: мы три раза прописывали свойства, фактически, с одинаковым функционалом.

Менялись только названия методов и локальных атрибутов. 

Представьте, во что превратится описание этого класса, если нужно будет задать 10 и более таких объектов-свойств!

Программист во всем этом просто запутается, да и редактировать такую программу станет непросто.
Как можно все это оптимизировать?

Здесь нам на помощь как раз и приходят дескрипторы.

Вначале, что вообще такое дескрипторы? Это класс, который содержит или один магический метод `__get__`:

```python
class A:
    def __get__(self, instance, owner): 
        return ...
```

Или класс, в котором дополнительно прописаны методы `__set__` и/или `__del__`:

```python
class B:
    def __get__(self, instance, owner):
        return ...
 
    def __set__(self, instance, value):
        ...
 
    def __del__(self):
        ...
```

Первый (класс A) называется `non-data descriptor` (дескриптор не данных), а второй (класс B) – data descriptor (дескриптор данных). 

Это различие имеет смысл, но об этом позже.

Как вы уже догадались, эти магические методы, по сути, `геттеры и сеттеры, а также делитер`.

Давайте, теперь посмотрим, как дескриптор может упростить наш программный код с обработкой координат точек.


Так как все координаты – целые числа, то интерфейс взаимодействия с ними мы определим через дескриптор с названием `Integer` (это имя мы, конечно же, придумываем сами):

```python
class Integer:
    def __set_name__(self, owner, name):
        self.name = "_" + name

# дескриптор не данных
```


Затем, в классе `Point3D` мы создадим три атрибута как объекты класса Integer:

```python
class Point3D:
    x = Integer()
    y = Integer()
    z = Integer()
 
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
```

Что делает класс `Integer`?

Просто создаёт локальные свойства!

А где сеттеры и геттеры? 

Добавим!

```python
class Integer: 

    def __set_name__(self, owner, name):
        self.name = "_" + name

    def __get__(self, instance, owner):
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        print(f"__set__: {self.name} = {value}")
        instance.__dict__[self.name] = value
```


Эти атрибуты и есть дескрипторы данных, через которые будет проходить взаимодействие.

 Итак, когда мы создавали экземпляры классов `Integer`, то 
* автоматически вызывался магический метод `__set_name__`, в котором параметр `self` являлся ссылкой на создаваемый экземпляр класса;

 * `owner` – ссылка на класс` Point3D`; 
 * `name` – имя атрибута (для первого объекта x, затем, y и z).
 
 В этом методе мы формируем локальное свойство с именем атрибута, добавляя перед ним одно нижнее подчеркивание (так принято делать при определении дескрипторов). 
 
 В итоге, в экземплярах классов будут храниться имена `_x, _y, _z`.

Зачем нам это нужно? Смотрите дальше. 

Предположим, мы создаем экземпляр класса `Point3D`:

```python
pt = Point3D(1, 2, 3)
```
Сработает инициализатор, а в нем идет обращение к дескрипторам `x, y, z`. 

В частности, мы им присваиваем переданные значения.

В этом случае, в классе `Integer` срабатывает сеттер (магический метод `__set__`), параметр `self` – это ссылка на объект дескриптора; `instance` – ссылка на объект pt, из которого произошло обращение к дескриптору; `value` – присваиваемое значение. 

В этом сеттере мы выводим в консоль сообщение, что был вызван данный метод и отображаем сохраненное имя и присваиваемое значение.

Следующей строчкой через ссылку `instance`, то есть, на экземпляр класса pt, формируем в нем локальное свойство с именем self.name и присваиваем значение value. 

В результате, в объекте pt появляются локальные свойства `_x, _y, _z` с соответствующими значениями.

Если затем, выполнить считывание данных через дескриптор, например, x, то автоматически сработает геттер (метод `__get__`), в котором `self` – это ссылка на объект `Integer; instance` – ссылка на экземпляр класса `pt`; owner – ссылка на класс `Point3D`. 

Мы здесь через ссылку instance обращаемся к словарю `__dict__` и считываем значение нужного локального свойства, которое, затем, возвращается геттером.

Это же значение автоматически возвращается и самим дескриптором.

Вот общая схема работы дескрипторов применительно к нашему классу `Point3D`. 

Теперь, сколько бы интерфейсов взаимодействия нам не понадобилось, мы легко их можем добавить в наш класс и все будет выглядеть понятно и компактно. 

На первый взгляд все это может показаться каким-то сложным и запутанным. 

Но, если внимательно во всем разобраться, то все предельно просто, только несколько громоздко. 

Именно громоздко, а не сложно. 

Поэтому, при необходимости, просто посмотрите несколько раз объяснение схемы и я уверен, каждый из вас поймет принцип ее работы. Ну а мы реализуем ее в нашей программе.

После создания экземпляра класса и вывода локальных свойств объекта:

```python
pt = Point3D(1, 2, 3)
print(pt.__dict__)
```
увидим в консоли следующие строчки:

```python
__set__: _x = 1
__set__: _y = 2
__set__: _z = 3
{'_x': 1, '_y': 2, '_z': 3}
```

Последнее, что нужно прописать в дескрипторе – это проверку корректности данных. 
Для этого у нас уже есть метод `verify_coord`, перенесем его в класс `Integer` и вызовем в сеттере:

```python
class Integer:
    @classmethod
    def verify_coord(cls, coord):
        if type(coord) != int:
            raise TypeError("Координата должна быть целым числом")
 
    def __set_name__(self, owner, name):
        self.name = "_" + name
 
    def __get__(self, instance, owner):
        return instance.__dict__[self.name]
 
    def __set__(self, instance, value):
        self.verify_coord(value)
        instance.__dict__[self.name] = value
```

Теперь, если при формировании объекта указать неверный тип данных:

```python
pt = Point3D('1', 2, 3)
```
то увидим сообщение об ошибке.

Еще в классе Integer я сделаю обращение к атрибутам экземпляра через стандартные функции `getattr` и `setattr`:

```python
    def __get__(self, instance, owner):
        return getattr(instance, self.name)
 
    def __set__(self, instance, value):
        self.verify_coord(value)
        setattr(instance, self.name, value)
```
Так будет правильнее, с точки зрения Python, чем обращение напрямую к специальной коллекции `__dict__`.

В итоге, мы с вами определили дескриптор данных (`data descriptor`) и на его основе создали три объекта `x, y, z` для интерфейса взаимодействия с координатами точки объектов класса `Point3D`.

Теперь вернемся к тому, что вначале я вам говорил, что важно различать дескрипторы данных и не данных. 

В чем разница? 

Ну, во-первых, очевидно, дескрипторы не данных не могут менять значения какого-либо свойства, так как не имеют сеттера и делитера.

Они служат только для считывания информации. И есть второе важное отличие. 

Они имеют тот же приоритет доступа, что и обычные атрибуты класса. О чем здесь речь? 

Давайте я покажу это различие на примере. Добавим в программу еще один дескриптор, только не данных:

```python
class ReadIntX:
    def __set_name__(self, owner, name):
        self.name = "_x"
 
    def __get__(self, instance, owner):
        return getattr(instance, self.name)
```
Он у нас будет считывать локальное свойство _x. Определим его в классе Point3D:
```python
    xr = ReadIntX()
```
И теперь можем использовать для считывания локального атрибута _x:

```python
print(pt.xr)
```
Как видите, все работает. Но, что будет, если мы запишем конструкцию:
```python
pt.xr = 5
```

Произойдет ошибка? Нет! 

В экземпляре pt будет создано новое локальное свойство с именем xr и мы в этом можем убедиться:

```python
print(pt.xr, pt.__dict__)

```
Кроме того, при обращении к `pt.xr` мы получаем значение 5, а не 1. 

Это, как раз и говорит о том, что приоритет доступа к локальным свойствам объекта и к дескриптору не данных одинаков.

Однако, если в дескриптор добавить сеттер и превратить его в дескриптор данных:
```python
class ReadIntX:
    def __set_name__(self, owner, name):
        self.name = "_x"
 
    def __get__(self, instance, owner):
        return getattr(instance, self.name)
 
    def __set__(self, instance, value):
        setattr(instance, self.name, value)
```

А создание локального атрибута xr в объекте pt мы сделаем через коллекцию `__dict__`:

```python
pt.__dict__['xr'] = 5

```
то при выполнении:
```python
print(pt.xr, pt.__dict__)
```

увидим значение `1`, хотя в объекте существует свойство `xr`. 

Это произошло потому, что приоритет обращению к дескриптору данных выше, чем к локальным атрибутам экземпляра класса.

То есть, здесь все работает ровно так, как и с доступом к объектам-свойствам, о которых мы говорили на прошлых занятиях.

На этом мы завершим с вами знакомство с этой непростой темой «дескрипторы». 

Постарался объяснить все, как можно проще. 

Знать этот функционал нужно, чтобы при необходимости не изобретать велосипед, а использовать уже встроенные возможности языка `Python`.